Skip to content

[Prism] Schedule consumers of a self checkpointing source - #39572

Open
Eliaaazzz wants to merge 1 commit into
apache:masterfrom
Eliaaazzz:fix-39446-prism-continuation-watermark
Open

[Prism] Schedule consumers of a self checkpointing source#39572
Eliaaazzz wants to merge 1 commit into
apache:masterfrom
Eliaaazzz:fix-39446-prism-continuation-watermark

Conversation

@Eliaaazzz

Copy link
Copy Markdown
Contributor

Fixes #39446.

The bug

An unbounded source SDF that returns ProcessContinuation.resume() has its residual re-queued by reElementResiduals as a pending element carrying the input element's event time, which for an Impulse rooted source is mtime.MinTimestamp. That pins minPendingTimestampLocked, so the stage's watermark never advances.

updateWatermarks returns no refreshes when the output watermark does not move, and PersistBundle marked only the producing stage as changed. So a consumer that had just been handed pending elements was never inserted into em.changedStages, bundleReady was never called for it, and its elements accumulated forever.

This is worse than the issue describes: it starves plain ordinary consumers too, not only stateful ones, and it survives transitively through a chain of stages. checkForQuiescence cannot catch it, because the source stage stays schedulable, so the job live locks rather than failing fast.

Reproduced at the element manager level before fixing. Pre-fix, across 5 source bundles:

round 0: src out=-inf | sink pending=1 scheduled=0
round 4: src out=-inf | sink pending=5 scheduled=0

The fix

  1. PersistBundle records the consumers that accepted data into stageState.consumersWithNewData, and updateWatermarks hands that set to the scheduler on the newOut <= ss.output path, clearing it on the advancing path where the normal return already lists every consumer. Routing the wake-up through updateWatermarks is what keeps a pipeline whose watermark advances on exactly its previous schedule.
  2. The recording is gated on em.sawResidual, latched the first time any bundle returns a residual. A pipeline that never self checkpoints never populates the set, so its bundle scheduling is unchanged. This matters: an earlier revision that recorded unconditionally changed bundle packing and broke TestRunner_Pipelines/flatten_to_sideInput at -count=20.
  3. bundleReady lets a stateful stage with no side inputs run on pending data alone, since statefulStageKind.buildEventTimeBundle already takes data at any watermark and gates only timers. Its stillSchedulable now requires buildable work so a key holding only a future timer cannot spin against the relaxed gate, and such a key no longer consumes the OneKeyPerBundle slot, is not marked in progress, and does not hold the bundle's minimum timestamp.

What is deliberately unchanged

  • The watermark value. BundleApplication.output_watermarks says an unreported estimate means MIN_TIMESTAMP, and Java's ProcessFn holds at futureOutputWatermark ?? elementTimestamp. A pinned MIN_TIMESTAMP output watermark for a source that reports no estimate is the spec, so the TODO the issue points at is not where the defect is.
  • Aggregate stages stay on the watermark gate. A GBK genuinely needs window closure or a trigger.
  • Any stage that reads a side input stays on the watermark gate, so such a consumer still waits under a residual pinned source. Side input readiness is derived from the main watermark, and GetSideData filters non global windows by the bundle watermark, so relaxing this would let a bundle read a partial or empty side input. That is worse than waiting. Fixing it properly means changing how side inputs are materialized for every stage, which I would rather do separately with your input.

Testing

New elementmanager_continuation_test.go, all teeth checked by reverting each half of the fix independently:

  • TestPersistBundle_ContinuationResidualConsumers, ordinary and stateful consumers of a source that self checkpoints forever.
  • TestPersistBundle_ContinuationResidualTransitive, src self checkpoints, mid returns no residual of its own, sink two stages down.
  • TestPersistBundle_ContinuationResidualWatermark, pins the output_watermarks contract in both directions.
  • TestStatefulBuildEventTimeBundle_OneKeyPerBundle, a key headed by a future timer must not consume the single key slot, and must not enter newKeys or hold minTs.

Green: go test ./pkg/beam/runners/prism/... -count=1, -run "TestRunner_Pipelines/flatten" -count=20, and the new tests at -count=50.


Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Choose reviewer(s) and mention them in a comment (R: @username).
  • Mention the appropriate issue in your description (for example: addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, comment fixes #<ISSUE NUMBER> instead.
  • Update CHANGES.md with noteworthy changes.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

See the Contributor Guide for more tips on how to make review process smoother.

To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions

Copy link
Copy Markdown
Contributor

Checks are failing. Will not request review until checks are succeeding. If you'd like to override that behavior, comment assign set of reviewers

Fixes apache#39446.

An unbounded source SDF that returns a process continuation residual has
that residual re-queued as a pending element carrying the input element's
event time, MinTimestamp for an Impulse rooted source, so the stage's
watermark never advances. updateWatermarks returns no refreshes when the
output watermark does not move, and PersistBundle marked only the
producing stage as changed, so a consumer that was just handed pending
elements was never surfaced to the scheduler. Its elements accumulated
forever. checkForQuiescence cannot catch this, because the source stays
schedulable, so the job live locks instead of failing fast.

PersistBundle now records the consumers that accepted data, and
updateWatermarks hands them to the scheduler on the path where the output
watermark does not advance. Recording is gated on any bundle having
returned a residual, so a pipeline that never self checkpoints keeps its
previous bundle scheduling.

bundleReady additionally lets a stateful stage with no side inputs run on
pending data alone, under the same gate, since
statefulStageKind.buildEventTimeBundle takes data at any watermark and
gates only timers. Its stillSchedulable now requires buildable work, and
a key that supplies nothing no longer consumes the OneKeyPerBundle slot,
is not marked in progress, and does not hold the bundle's minimum
timestamp.

A consumer that reads a side input still waits on the watermark, since
side input readiness is derived from it.
@Eliaaazzz
Eliaaazzz force-pushed the fix-39446-prism-continuation-watermark branch from d147583 to feddf87 Compare August 1, 2026 00:09
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Assigning reviewers:

R: @jrmccluskey for label go.

Note: If you would like to opt out of this review, comment assign to next reviewer.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

The PR bot will only process comments in the main thread (not review comments).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Prism] Watermark deadlock when unbounded source is initially empty

1 participant